<!-- 由于有查询样式的需求，程序处理放在mounted之后 -->
<template>
	<view class="vi_circle" :style="{ 'width':W, 'height':W }">
		<view :id="id" class="vi_circle_getStyle" :class="'vi_'+setting.type" :style="{ 'width':W, 'height':W }"></view>
		<canvas class="vi_circle_canvas vi_bg" :canvas-id="id+'-canvas-bg'" :id="id+'-canvas-bg'" :style="{ width: W, height: W }"></canvas>
		<canvas class="vi_circle_canvas vi_act" :canvas-id="id+'-canvas-act'" :id="id+'-canvas-act'" :style="{ width: W, height: W }"></canvas>
		<view class="vi_circle_center">
			<slot>
				<text :style="{ 'font-size': `calc(${(R - barW)*0.5}px - ${decimal/2}em)` }">{{oldValue.toFixed(decimal)}}%</text>
			</slot>
		</view>
	</view>
</template>

<script>
	export default {
		name:"viCircle",
		props:{
			//页面 组件id必须唯一， 且必须传入。APP无法自动生成
			id:{
				type:String,
				default: "",
				require:true
			},
			//初始渲染使用动画，非H5环境默认关闭。微信多个canvas动画同时渲染会有严重的性能问题
			initAnimation:{//已内含
				type:[Boolean,String,null],
				default: ""
			},
			//组件尺寸，半径
			radius:{
				type:[String,Number,null],
				default:""
			},
			//环形条线宽，取组件宽的百分比
			barWidthPercent:{
				type:[Number,null],
				default:null
			},
			barBgColor:{
				type:[String,null],
				default:""
			},
			//预置颜色类型
			type:{
				type:[String,null],
				default:null
			},
			//环形条颜色，可填入渐变色，格式: {"0": 颜色值1, "100": 颜色值2 }
			barColor:{
				type:[String,Object,null],
				default:""
			},
			//渐变色角度，barColor填入渐变色时有效，默认上到下
			barColorAngle:{
				type:[Number,null],
				default:0
			},
			//动态环形条渐变颜色，barColor填入渐变色时有效，优先于静态渐变
			barColorActive:{
				type:[Boolean,null],
				default: false
			},
			//环形条起始角度
			startAngle:{
				type:[Number,null],
				default:0
			},
			value:{
				type:[Number,null],
				default:0
			},
			//初始自动执行动画
			autoStart:{
				type:[Boolean, null],
				default: null
			},
			//动画时间，走完整圈需要的时间
			duration:{
				type:[Number,null],
				default: 0
			},
			//帧率，一般不需要更改，帧率过高会使动画延迟
			frameRate: {
				type:[Number,null],
				default: 0
			},
			//小数位，用于中间数值显示
			decimal:{
				type:[Number,null],
				default: 0
			},
			//背景加速，背景条速度比环形条快
			barBgSpeedRate:{
				type:[Number,null],
				default: 0
			}
		},
		data() {
			return {
				width: 0,//组件宽，组件渲染后获取
				colorObj: "#000",//暂存环形条颜色最终值，用于渲染
				tepColors: [],
				inValue: 0,
				oldValue: 0,
				bgHandle: null,
				actHandle: null,
				actTimer:null,//动画定时器
				bgTimer:null,//背景动画定时器
				isRun:false,//动画是否执行中，用来判断最后一次动画是否要执行（根据步阶，最后帧数值不一定是整数，需要补帧）
			}
		},
		computed:{
			//将默认值内含到css、计算属性等地方，只内含真值
			//规避特殊使用的情况下，由prop传入undefined、null等无效数据导致的，默认值失效问题。例如组件嵌套且需要组件间参数传递
			//HBuilder X3.3.13; 微信开发者工具stable 1.05.2111300
			//微信小程序会对传入的undefined、null值根据参数类型输出不同空值(false、""、null、0)，参数类型为null或未指定参数类型则输出null
			//H5会对传入的undefined取默认值，未设默认值则输出undefined。对传入的null值固定输出null
			//做参数传递时需警惕两者行为不同而造成的陷阱，主要问题出现在微信小程序的空值转化，输出非用户传入的空值、假值，使默认真值丢失
			//兼容空值取默认值的可行方案目前为，参数类型加入null如[Boolean,null]，判断传入参数是否为定义的类型，是则使用，否则赋予默认值
			setting(){
				return {
					type: this.$api.isDefined(this.type)? this.type :"primary",
					frameRate: this.frameRate ||60,
					radius: this.radius ||"80rpx",
					barWidthPercent: this.$api.isValue(this.barWidthPercent)? this.barWidthPercent :10,
					barBgColor: this.barBgColor ||"#eee",
					autoStart: this.$api.isBoolean(this.autoStart)? this.autoStart :true,
					duration: this.duration ||1500,
					barBgSpeedRate: this.barBgSpeedRate ||3,
				}
			},
			//盒子宽(带单位)
			W(){
				let radius = this.setting.radius;
				if(this.$api.isNumber(radius, true)){
					return radius *2 +"rpx"
				}else{
					let valUnit = this.$api.splitUnit(radius);
					return valUnit[0] *2 + valUnit[1]
				}
			},
			//实际半径
			R(){
				if(this.width){
					return this.width /2
				}
				return 0
			},
			//线条宽，默认单位px
			barW(){
				if(this.width){
					return Math.round(this.width * this.setting.barWidthPercent /100) ||1
				}
				return 0
			},
			//画弧起始点，减量
			origin(){
				return -0.5 * Math.PI
			},
			//起始弧度
			startPoint(){
				return this.startAngle * Math.PI /180
			},
			//步阶，每帧的数值增量。根据动画时间和帧率动态决定
			tep(){
				return 100000 / (this.setting.frameRate * this.setting.duration)
			},
		},
		methods: {
			getStyle(callback){
				//获取主题色、组件实际宽。由于颜色值来自css，需实际渲染后再通过样式查询获取
				// #ifdef H5
					let style = window.getComputedStyle(document.getElementById(this.id));
					this.width = this.$api.splitUnit(style.width)[0];
					this.colorObj = this.$api.colorConvert(style.color);
					this.resetColor();
					if(callback){
						callback()
					}
				// #endif
				// #ifndef H5
					const views = uni.createSelectorQuery().in(this);
					views.select(`#${this.id}`)
						.fields({
							computedStyle: ["color", "width"]
						}, (data) => {
							this.width = this.$api.splitUnit(data.width)[0];
							this.colorObj = this.$api.colorConvert(data.color);
							this.resetColor();
							if(callback){
								callback()
							}
						})
						.exec()
				// #endif
			},
			resetColor(){
				//如有传入颜色值，则重置颜色
				if(this.barColor){
					if(typeof(this.barColor)=="string"){
						this.colorObj = this.$api.colorConvert(this.barColor)
					}else{//创建渐变
						if(!this.barColorActive){//静态渐变色
							let originX, originY, R, angle = this.barColorAngle%360;
							let x1, y1, x2, y2;
							originX = originY = R = this.R;
							x1 = originX + Math.sin(angle * Math.PI / 180 ) * R;
							y1 = originY - Math.cos(angle * Math.PI / 180 ) * R;
							x2 = originX + Math.sin( (angle + 180)%360 * Math.PI / 180 ) * R;
							y2 = originY - Math.cos( (angle + 180)%360 * Math.PI / 180 ) * R;
							//生成canvas颜色组
							this.colorObj = this.bgHandle.createLinearGradient(x1, y1, x2, y2);
							Object.keys(this.barColor).sort((a, b) => {
								return Number(a) - Number(b)
							}).map((key) => {
								this.colorObj.addColorStop(Number(key) / 100, this.barColor[key])
							});
						}else{//动态渐变色，生成环形颜色列表
							Object.keys(this.barColor).sort((a, b) => {
								return Number(a) - Number(b)
							}).map((key) => {
								this.tepColors.push({
									value: Number(key),
									color: this.$api.colorConvert(this.barColor[key], "Array")
								});
							});
						}
					}
				}
			},
			start(isInit){
				clearTimeout(this.actTimer);
				clearTimeout(this.bgTimer);
				this.isRun=true;
				this.$emit("start");
				let initAnimation;
				// #ifdef H5
				initAnimation = this.$api.isBoolean(this.initAnimation)? this.initAnimation :true
				// #endif
				// #ifndef H5
				initAnimation = this.$api.isBoolean(this.initAnimation)? this.initAnimation :false
				// #endif
				if(isInit && !initAnimation){//不使用动画，立即完成
					this.drawBg(100);
					this.drawAct(this.inValue);
				}else{
					this.drawBg();
					this.drawAct();
				}
			},
			drawBg(value=0){//value是百分比
				let sAngle = this.origin + this.startPoint;
				let eAngle = this.origin + this.startPoint + value * Math.PI /50;
				this.bgHandle.setLineCap('round');
				this.bgHandle.setLineWidth(this.barW);
				this.bgHandle.setStrokeStyle(this.setting.barBgColor);
				this.bgHandle.beginPath();
				this.bgHandle.arc(this.R, this.R, this.R - this.barW, sAngle, eAngle, false);
				this.bgHandle.stroke();
				this.bgHandle.draw();
				if(value<100){
					value += this.tep;
					this.bgTimer = setTimeout(() => {
						this.drawBg(value);
					}, 1000/(this.setting.frameRate*this.setting.barBgSpeedRate));//错位时间，背景速度比环形条快
				}
			},
			drawAct(value=0){
				let sAngle = this.origin + this.startPoint;
				let eAngle = this.origin + this.startPoint + value * Math.PI /50;
				if(this.barColorActive){//动态渐变色，重新设置当前颜色值。用数字计算后转为颜色字符串
					let point = 0;
					for(let i=0; i<this.tepColors.length; i++){
						if(value >= this.tepColors[i].value){
							point = i
						}else{
							break
						}
					}
					//计算当前进度颜色值
					let curr, next;
					if(value==0){//起点
						this.colorObj = this.$api.colorConvert(this.tepColors[0].color)
					}else if(point+1 >= this.tepColors.length){//末尾
						this.colorObj = this.$api.colorConvert(this.tepColors[point].color)
					}else{
						let pct=(value - this.tepColors[point].value)/(this.tepColors[point+1].value - this.tepColors[point].value),
							currR=this.tepColors[point].color[0],
							currG=this.tepColors[point].color[1],
							currB=this.tepColors[point].color[2],
							nextR=this.tepColors[point+1].color[0],
							nextG=this.tepColors[point+1].color[1],
							nextB=this.tepColors[point+1].color[2];
						this.colorObj = this.$api.colorConvert([
							currR+(nextR-currR)*pct,
							currG+(nextG-currG)*pct,
							currB+(nextB-currB)*pct
						]);
					}
				}
				this.actHandle.setLineCap('round');
				this.actHandle.setLineWidth(this.barW);
				this.actHandle.setStrokeStyle(this.colorObj);
				this.actHandle.beginPath();
				this.actHandle.arc(this.R, this.R, this.R - this.barW, sAngle, eAngle, false);
				this.actHandle.stroke();
				if(value===0){//如果进度为0，清空画布
					this.actHandle.clearRect(0,0,this.width,this.width)
				}
				this.actHandle.draw();
				this.oldValue = value;
				
				if(!this.isRun){//最后一次绘画执行完毕，在这里结束
					this.$emit("stop");
					return
				}
				if(this.inValue > value) {//增大
					value += this.tep;
					if(value > this.inValue){//达到目标值，去执行最后一次绘画
						value = this.inValue;
						this.isRun = false
					}
				}else{//减少
					value -= this.tep;
					if(value < this.inValue){//达到目标值，去执行最后一次绘画
						value = this.inValue
						this.isRun = false
					}
				}
				this.actTimer = setTimeout(() => {
					this.drawAct(value);
				}, 1000/this.setting.frameRate);
			},
		},
		watch:{
			"value":{
				handler(newVal){
					this.inValue = newVal;
					clearTimeout(this.actTimer);
					this.isRun=true;
					this.$emit("start");
					this.drawAct(this.oldValue);
				}
			}
		},
		created() {
			this.bgHandle = uni.createCanvasContext(this.id+"-canvas-bg", this);
			this.actHandle = uni.createCanvasContext(this.id+"-canvas-act", this);
			this.inValue = this.value ||0;
			
			this["refresh"] = this.$api.throttle(()=>{
				this.getStyle(()=>{
					this.start();
				});
			}, {type:"last", wait:200})
		},
		mounted() {
			//查询样式。完成后执行动画
			this.getStyle(()=>{
				let start = this.setting.autoStart;
				if(start){
					setTimeout(()=>{
						this.start(true)
					},200)
				}
			});
			
			//监听窗口变化
			uni.onWindowResize(()=>{
				this.refresh();
			})
		},
		destroyed() {
			uni.offWindowResize(()=>{})
		}
	}
</script>

<style lang="scss">
$bar-background-color:#eee;
$center-background-color:#fff;
.vi_circle{
	position: relative;
	display: flex;
	justify-content: center;
	align-items: center;
	.vi_circle_getStyle{
		position: absolute;
		z-index: -1;
		opacity: 0;
	}
	.vi_circle_canvas{
		position: absolute;
		left: 0;
		top: 0;
	}
	.vi_circle_center{
		position: absolute;
		display: flex;
		justify-content: center;
		align-items: center;
		background-color: $center-background-color;
		color: none;
	}
	//颜色样式
	.vi_primary{
		color: $vi-color-primary;
	}
	.vi_second{
		color: $vi-color-second;
	}
	.vi_third{
		color: $vi-color-third;
	}
	.vi_fourth{
		color: $vi-color-fourth;
	}
	.vi_fifth{
		color: $vi-color-fifth;
	}
}
</style>
